Read more of this story at Slashdot.
Read more of this story at Slashdot.
A customer was baffled by crash reports that indicated that their program was failing on its very first instruction.
I opened one of the crash dumps, and it was so weird, the debugger couldn’t even say what went wrong.
ERROR: Unable to find system thread FFFFFFFF ERROR: The thread being debugged has either exited or cannot be accessed ERROR: Many commands will not work properly This dump file has an exception of interest stored in it. The stored exception information can be accessed via .ecxr. ERROR: Exception C0000005 occurred on unknown thread FFFFFFFF (61c.ffffffff): Access violation - code c0000005 (first/second chance not available) 0:???> r WARNING: The debugger does not have a current process or thread WARNING: Many commands will not work ^ Illegal thread error in 'r' 0:???> .ecxr WARNING: The debugger does not have a current process or thread WARNING: Many commands will not work 0:???>
Let’s see what threads we have.
0:???> ~ WARNING: The debugger does not have a current process or thread WARNING: Many commands will not work 0 Id: 61c.12b4 Suspend: 1 Teb: 000000c7`9604d000 Unfrozen 1 Id: 61c.22d4 Suspend: 1 Teb: 000000c7`9604f000 Unfrozen 2 Id: 61c.1ab0 Suspend: 1 Teb: 000000c7`96051000 Unfrozen 3 Id: 61c.3308 Suspend: 1 Teb: 000000c7`96053000 Unfrozen 4 Id: 61c.2af0 Suspend: 1 Teb: 000000c7`96055000 Unfrozen 5 Id: 61c.2054 Suspend: 1 Teb: 000000c7`96059000 Unfrozen 0:???>
I wonder what they are doing.
We’ll switch to each thread just to see what instruction they are at
0:???> ~0s WARNING: The debugger does not have a current process or thread WARNING: Many commands will not work ntdll!RtlUserThreadStart: 00007ffa`bb16df50 4883ec78 sub rsp,78h 0:000> ~*s ^ Illegal thread error in '~*s' 0:000> ~1s 00000293`42074058 66894340 mov word ptr [rbx+40h],ax ds:00007ff6`e4600040=1f0e 0:001> ~2s ntdll!ZwWaitForWorkViaWorkerFactory+0x14: 00007ffa`bb1b29c4 c3 ret 0:002> ~3s ntdll!ZwWaitForWorkViaWorkerFactory+0x14: 00007ffa`bb1b29c4 c3 ret 0:003> ~4s ntdll!ZwWaitForWorkViaWorkerFactory+0x14: 00007ffa`bb1b29c4 c3 ret 0:004> ~5s ntdll!ZwDelayExecution+0x14: 00007ffa`bb1af3f4 c3 ret
The ostensible reason for the crash was an invalid write instruction, and only thread 1 is doing a write. Let’s take a closer look at what it’s trying to write to.
0:001> !address @rbx Usage: Image Base Address: 00007ff6`e4600000 End Address: 00007ff6`e4601000 Region Size: 00000000`00001000 ( 4.000 kB) State: 00001000 MEM_COMMIT Protect: 00000002 PAGE_READONLY Type: 01000000 MEM_IMAGE Allocation Base: 00007ff6`e4600000 Allocation Protect: 00000080 PAGE_EXECUTE_WRITECOPY Image Path: C:\Program Files\Contoso\ContosoDeluxe.exe Module Name: ContosoDeluxe Loaded Image Name: ContosoDeluxe.exe Mapped Image Name: C:\Program Files\Contoso\ContosoDeluxe.exe More info: lmv m ContosoDeluxe More info: !lmi ContosoDeluxe More info: ln 0x7ff6e4600000 More info: !dh 0x7ff6e4600000 Content source: 2 (mapped), length: 400 0:001> ln @rbx (00000000`00000000) ContosoDeluxe!__ImageBase
Okay, so we are writing to the mapped image header for ContosoDeluxe itself. This is a read-only page (PAGE_READONLY
), which is why we take a write access violation.
In fact, we’re writing into the image header, which is not something anybody normally does. This looks quite suspicious.
If we ask for stacks, we get this:
0:001> ~*k 0 Id: 61c.12b4 Suspend: 1 Teb: 000000c7`9604d000 Unfrozen Child-SP RetAddr Call Site 000000c7`962ffd48 00000000`00000000 ntdll!RtlUserThreadStart 1 Id: 61c.22d4 Suspend: 1 Teb: 000000c7`9604f000 Unfrozen Child-SP RetAddr Call Site 000000c7`963ff900 00007ff6`e4600000 0x00000293`42074058 2 Id: 61c.1ab0 Suspend: 1 Teb: 000000c7`96051000 Unfrozen Child-SP RetAddr Call Site 000000c7`964ff718 00007ffa`bb145a0e ntdll!ZwWaitForWorkViaWorkerFactory+0x14 000000c7`964ff720 00007ffa`ba25244d ntdll!TppWorkerThread+0x2ee 000000c7`964ffa00 00007ffa`bb16df78 kernel32!BaseThreadInitThunk+0x1d 000000c7`964ffa30 00000000`00000000 ntdll!RtlUserThreadStart+0x28 3 Id: 61c.3308 Suspend: 1 Teb: 000000c7`96053000 Unfrozen Child-SP RetAddr Call Site 000000c7`965ff6a8 00007ffa`bb145a0e ntdll!ZwWaitForWorkViaWorkerFactory+0x14 000000c7`965ff6b0 00007ffa`ba25244d ntdll!TppWorkerThread+0x2ee 000000c7`965ff990 00007ffa`bb16df78 kernel32!BaseThreadInitThunk+0x1d 000000c7`965ff9c0 00000000`00000000 ntdll!RtlUserThreadStart+0x28 4 Id: 61c.2af0 Suspend: 1 Teb: 000000c7`96055000 Unfrozen Child-SP RetAddr Call Site 000000c7`966ffad8 00007ffa`bb145a0e ntdll!ZwWaitForWorkViaWorkerFactory+0x14 000000c7`966ffae0 00007ffa`ba25244d ntdll!TppWorkerThread+0x2ee 000000c7`966ffdc0 00007ffa`bb16df78 kernel32!BaseThreadInitThunk+0x1d 000000c7`966ffdf0 00000000`00000000 ntdll!RtlUserThreadStart+0x28 5 Id: 61c.2054 Suspend: 1 Teb: 000000c7`96059000 Unfrozen Child-SP RetAddr Call Site 000000c7`968ffcb8 00007ffa`bb165833 ntdll!ZwDelayExecution+0x14 000000c7`968ffcc0 00007ffa`b88f9fcd ntdll!RtlDelayExecution+0x43 000000c7`968ffcf0 00000293`420a1efd KERNELBASE!SleepEx+0x7d 000000c7`968ffd70 00000000`00000000 0x00000293`420a1efd
Thread 1 is the suspicious thread that committed the access violation.
There’s another suspicious thread, thread 5, which is in a SleepEx
call called from the same suspicious source 0x00000293`420xxxxx
. This other thread is probably waiting for something to happen, so let’s take a look at it.
First, let’s see what kind of memory we are executing from.
0:001> !address 00000293`420a1ee0 Usage: <unknown> Base Address: 00000293`420a0000 End Address: 00000293`420ca000 Region Size: 00000000`0002a000 ( 168.000 kB) State: 00001000 MEM_COMMIT Protect: 00000040 PAGE_EXECUTE_READWRITE Type: 00020000 MEM_PRIVATE Allocation Base: 00000293`420a0000 Allocation Protect: 00000040 PAGE_EXECUTE_READWRITE
Yikes, PAGE_EXECUTE_READWRITE
. That’s not a good sign. That smells like malicious code injection, because it is highly unusual for normal code to be read-write. But let’s hold out hope that maybe there’s a legitimate explanation for all of this, and it’s just a matter of finding it.
Let’s see what code we are executing.
00000293`420a1ed9 add rsp,30h 00000293`420a1edd pop rdi 00000293`420a1ede ret 00000293`420a1edf int 3 00000293`420a1ee0 push rbx 00000293`420a1ee2 sub rsp,20h 00000293`420a1ee6 call 00000293`420a13e0 00000293`420a1eeb mov qword ptr [00000293`420c0c78],rax 00000293`420a1ef2 mov ecx,3E8h 00000293`420a1ef7 call qword ptr [00000293`420b4028] ^^^^^^^^ YOU ARE HERE 00000293`420a1efd call 00000293`420a13e0 // do it again 00000293`420a1f02 mov rdx,rax 00000293`420a1f05 mov rbx,rax 00000293`420a1f08 call 00000293`420a19d0 00000293`420a1f0d test eax,eax 00000293`420a1f0f jne 00000293`420a1f22 00000293`420a1f11 mov rax,qword ptr [00000293`420c0c78] 00000293`420a1f18 mov qword ptr [00000293`420c0c78],rbx 00000293`420a1f1f mov rbx,rax 00000293`420a1f22 mov rcx,rbx 00000293`420a1f25 call 00000293`420a17f0 00000293`420a1f2a jmp 00000293`420a1ef2
The first few instructions, up to the int 3
appear to be the end of the previous function, so we can start our analysis at the push rbx
.
push rbx ; preserve register sub rsp, 20h ; stack frame call 00000293`420a13e0 ; mystery function 1 mov [00000293`420c0c78],rax ; save answer in global 00000293`420a1ef2: mov ecx, 3E8h ; decimal 1000 call [00000293`420b4028] ; mystery function 2 ^^^^^^^^ YOU ARE HERE call 00000293`420a13e0 ; mystery function 1 mov rdx, rax ; return value becomes param1 mov rbx, rax ; save return value in rbx call 00000293`420a19d0 ; mystery function 3 test eax,eax ; Q: did it succeed? jne 00000293`420a1f22 ; N: Skip mov rax, [00000293`420c0c78] ; get previous value mov [00000293`420c0c78], rbx ; replace with new value mov rbx, rax ; save previous value in rbx 00000293`420a1f22: mov rcx, rbx ; rcx = updated value in rbx call 00000293`420a17f0 ; mystery function 3 jmp 00000293`420a1ef2 ; loop back forever
One thing that’s apparent here is that this thread never exits. It’s an infinite loop.
First, let’s see if we can identify the mystery functions.
The easiest is probably mystery function 2, since it looks like a call to an imported function.
0:001> dps 00000293`420b4028 L1 00000293`420b4028 00007ffa`ba258370 kernel32!SleepStub
Aha, mystery function 2 is Sleep
, and the call is a Sleep(1000)
. Which we sort of knew from the stack trace but it’s nice to see confirmation.
But let’s look around near that address, since that may be part of a larger table of function pointers.
00000293`420b4000 00007ffa`baa59810 advapi32!RegCloseKeyStub 00000293`420b4008 00007ffa`baa596e0 advapi32!RegQueryInfoKeyWStub 00000293`420b4010 00007ffa`baa595a0 advapi32!RegOpenKeyExWStub 00000293`420b4018 00007ffa`baa5ab30 advapi32!RegEnumValueWStub 00000293`420b4020 00000000`00000000 00000293`420b4028 00007ffa`ba258370 kernel32!SleepStub 00000293`420b4030 00007ffa`ba250cc0 kernel32!GetLastErrorStub 00000293`420b4038 00007ffa`ba266b60 kernel32!lstrcatW 00000293`420b4040 00007ffa`ba25ff00 kernel32!CloseHandle 00000293`420b4048 00007ffa`ba254380 kernel32!CreateThreadStub
Bingo, this appears to be a table of imported function pointers.
Mystery function 1 seems to be called to start things off, and then again in a loop, so it seems kind of important. Let’s see what it is.
00000293`420a13e0 mov qword ptr [rsp+8],rbx 00000293`420a13e5 mov qword ptr [rsp+10h],rsi 00000293`420a13ea mov qword ptr [rsp+18h],rdi 00000293`420a13ef push rbp 00000293`420a13f0 mov rbp,rsp 00000293`420a13f3 sub rsp,80h 00000293`420a13fa mov rax,qword ptr [00000293`420bf010] 00000293`420a1401 xor rax,rsp 00000293`420a1404 mov qword ptr [rbp-8],rax 00000293`420a1408 mov ecx,40h 00000293`420a140d call 00000293`420a8478 // mystery function 3
This looks like a typical C function, not hand-coded assembly. After saving non-volatile registers, it builds a stack frame, and the mov rax, [global]
followed by a xor rax, rsp
looks a lot like a /GS stack canary.
So at least it’s nice that this rogue code was compiled with stack buffer overflow protection. Can’t be too careful.
Let’s look at mystery function 3.
00000293`420a8478 push rbx sub rsp, 20h mov rbx, rcx jmp 00000293`420a8492 00000293`420a8483 mov rcx, rbx call 00000293`420aad50 test eax, eax je 00000293`420a84a2 mov rcx, rbx 00000293`420a8492 call 00000293`420aadb4 test rax, rax je 00000293`420a8483 add rsp, 20h pop rbx ret 00000293`420a84a2 cmp rbx, 0FFFFFFFFFFFFFFFFh je 00000293`420a84ae call 00000293`420a8c80 int 3 00000293`420a84ae call 00000293`420a8ca0 int 3 00000293`420a84b4 jmp 00000293`420a8478
This reverse-compiles to
uint64_t something(uint64_t value) { uint64_t p; while (uint64_t p = func00000293420aadb4(value); !p) { if (!func00000293420aad50(value)) { if (value == ~0ULL) { func00000293420a8c80(); } else { func00000293420a8c80(); } // NOTREACHED } } return p; }
This seems to call a function at func00000293420aadb4
repeatedly.
00000293`420aadb4 jmp 00000293`420acf8c
This appears to be an incremental linking thunk. So whatever this is, it looks like it was compiled in debug mode.
00000293`420acf8c push rbx sub rsp, 20h mov rbx,rcx cmp rcx, 0FFFFFFFFFFFFFFE0h ja 00000293`420acfd7 test rcx, rcx mov eax, 1 cmove rbx, rax jmp 00000293`420acfbe 00000293`420acfa9 call 00000293`420b02c0 test eax, eax je 00000293`420acfd7 mov rcx, rbx call 00000293`420aad50 test eax, eax je 00000293`420acfd7 00000293`420acfbe mov rcx, [00000293`420c07f8] mov r8, rbx xor edx, edx call [00000293`420b4298] test rax, rax je 00000293`420acfa9 jmp 00000293`420acfe4 00000293`420acfd7 call 00000293`420ac71c mov [rax], 0Ch xor eax, eax add rsp, 20h pop rbx ret
The initial comparison against 0xFFFFFFFF`FFFFFFFE
makes me suspect that this is malloc()
or operator new
because those functions begin with a check for an excessive allocation size, to avoid integer overflow.
And indeed, that’s basically what this function is, as revealed by the indirect function call:
0:005> dps 00000293`420b4298 L1 00000293`420b4298 00007ffa`bb14cca0 ntdll!RtlAllocateHeap
Okay, so we found malloc()
or operator new
.
This will help us understand mystery function 1 a lot better.
00000293`420a13e0 mov [rsp+8], rbx mov [rsp+10h], rsi mov [rsp+18h], rdi push rbp mov rbp, rsp sub rsp, 80h mov rax, [00000293`420bf010] xor rax, rsp mov [rbp-8], rax ; /GS canary mov ecx, 40h call 00000293`420a8478 ; allocate 64 bytes xorps xmm0, xmm0 mov ecx, 18h mov rdi,rax ; save first allocation movups [rax],xmm0 ; zero out first allocation movups [rax+10h],xmm0 movups [rax+20h],xmm0 movups [rax+30h],xmm0 call 00000293`420a8478 ; allocate 24 bytes xor esi,esi mov ecx, 80h mov rbx,rax ; save second allocation mov [rax+0Ch], rsi ; zero out second allocation mov [rax+14h], esi mov [rax], esi mov [rax+4], 10h mov [rax+8], 1 call 00000293`420a84b4 ; mystery function 4 mov [rbx+10h], rax ; save result lea ecx, [rsi+10h] ; ecx = 0x10 mov [rdi], rbx call 00000293`420a8478 ; third allocation lea ecx, [rsi+40h] ; ecx = 0x40 mov rbx, rax mov [rax+8], rsi ; initialize third allocation mov [rax], esi mov [rax+4], 10h call 00000293`420a84b4 ; mystery function 4 mov [rbx+8], rax lea ecx, [rsi+18h] ; ecx = 0x18
Okay, so this function starts by allocating many memory blocks and initializing them.
Let’s skip ahead to where it finally does something interesting.
lea rdx, [00000293`420bba90] ; LR"(SOFTWARE\systemconfig)" lea rax, [rbp-50h] mov [rdi+38h], rbx mov r9d, 20119h ; KEY_READ mov [rsp+20h], rax xor r8d, r8d mov rcx,0FFFFFFFF80000002h ; HKEY_LOCAL_MACHINE call qword ptr [00000293`420b4010] ; RegOpenKeyExW test eax, eax
A dps 00000293`420b4010
reveals that the function pointer is RegOpenKeyExW
, so the entire function call must have been
RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"SOFTWARE\\systemconfig", 0, KEY_READ, &key);
Further disassembly shows that if the code successfully opens the key, it tries to read some values from it. My guess is that systemconfig
is where the code stores its state.
Okay, so maybe I can speed things up by dumping strings and seeing if there’s anything that will give me a clue about the identity of this code. Recall that the !address
command told us that the memory block was
0:001> !address 00000293`420a1ee0 Base Address: 00000293`420a0000 End Address: 00000293`420ca000
We’ll ask the !mex debugger extension to find any strings in the memory block.
0:005> !mex.strings 00000293`420a0000 00000293`420ca000 ... 00000293420bbd10 system 00000293420bc1d4 H:\rootkit\r77-rootkit-master\vs\x64\Release\r77-x64.pdb
Okay, so I guess it’s malware, or at least self-identifies as a rootkit. And, hey, an Internet search for this rootkit name shows that its source code is public.
The good news for the developer is that the problem is not their fault. The bad news is that since the crash dumps are submitted anonymously, they have no way of contacting the users to tell them that they have been infected with malware.
The post The case of a program that crashed on its first instruction appeared first on The Old New Thing.
A lesson about the software forensics process involved in developing the Bring Back Plus/Minus extension, which brings back the plus/minus symbols to the editor outlining feature in Visual Studio 2022.
While I did have the advantage of being able to look at the Visual Studio source code, I could have figured this out just as well without it, and that’s the focus of this article.
The investigation of how to bring back the plus/minus symbols started with this important clue from a comment on the Developer Community feedback ticket for this issue:
However, Visual Studio has a very rich extensibility model, and if users have strong feelings about the visuals in their IDE, I encourage people to try writing an extension to change the icon used here. The class that controls the expansion is this one: OutliningMarginHeaderControl
A quick internet search for the class name led me to this reference article for OutliningMarginHeaderControl Class where the definition contains the following valuable information:
Microsoft.VisualStudio.Text.Editor
Microsoft.VisualStudio.Text.UI.Wpf.dll
Microsoft.VisualStudio.Text.UI.Wpf v17.9.187
Now that we know where to find this class, it’s time to examine the assembly. For that we will use the excellent ILSpy tool, which you can install from the Microsoft Store: ILSpy Fresh
Once you’ve installed ILSpy, launch it and load the assembly Microsoft.VisualStudio.Text.UI.Wpf.dll
. You can find this assembly in the Visual Studio installation folder, usually in the following path:
<VSInstallDir>\Common7\IDE\CommonExtensions\Microsoft\Editor\Microsoft.VisualStudio.Text.UI.Wpf.dll
After the assembly is loaded, search for the class OutliningMarginHeaderControl
in the Microsoft.VisualStudio.Text.Editor
namespace and you will find the following code for the static constructor:
This is where the default style key for the control is being set. The next step is to find the XAML Style
for the control. For that we open the Resources
node where we find themes/generic.baml
, which includes the entire style for the OutliningMarginHeaderControl
:
<Style x:Key="{x:Type textUiWpf:OutliningMarginHeaderControl}" TargetType="{x:Type textUiWpf:OutliningMarginHeaderControl}">
<Style.Resources>
<ResourceDictionary>
<Geometry x:Key="ExpandRight">F1M2.146.146a.5.5,0,0,1,.708,0l4,4a.5.5,0,0,1,0,.708l-4,4a.5.5,0,0,1-.708-.708L5.793,4.5,2.146.854A.5.5,0,0,1,2.146.146Z</Geometry>
<Geometry x:Key="ExpandDown">F1M8.854,2.146a.5.5,0,0,1,0,.708l-4,4a.5.5,0,0,1-.708,0l-4-4a.5.5,0,0,1,.708-.708L4.5,5.793,8.146,2.146A.5.5,0,0,1,8.854,2.146Z</Geometry>
</ResourceDictionary>
</Style.Resources>
<Setter Property="Focusable" Value="False" />
<Setter Property="FrameworkElement.Cursor" Value="Hand" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type textUiWpf:OutliningMarginHeaderControl}">
<Grid>
<Viewbox Name="ExpandCollapseIcon" Width="9" Height="13" VerticalAlignment="Center">
<Border Width="9" Height="13" Background="{DynamicResource outlining.chevron.background}">
<Rectangle Name="ExpandCollapseRectangle" Width="9" Height="9">
<FrameworkElement.Resources>
<ResourceDictionary>
<SolidColorBrush x:Key="canvas" Opacity="0" />
</ResourceDictionary>
</FrameworkElement.Resources>
<Shape.Fill>
<DrawingBrush Stretch="None">
<DrawingBrush.Drawing>
<DrawingGroup>
<DrawingGroup>
<GeometryDrawing Brush="{DynamicResource canvas}" Geometry="F1 M9,0 L9,9 L0,9 L0,0" />
</DrawingGroup>
<DrawingGroup>
<GeometryDrawing Brush="{DynamicResource outlining.chevron.foreground}" Geometry="{StaticResource ExpandRight}" />
</DrawingGroup>
</DrawingGroup>
</DrawingBrush.Drawing>
</DrawingBrush>
</Shape.Fill>
</Rectangle>
</Border>
</Viewbox>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="textUiWpf:OutliningMarginHeaderControl.IsExpanded" Value="True">
<Setter TargetName="ExpandCollapseRectangle" Property="Shape.Fill">
<Setter.Value>
<DrawingBrush Stretch="None">
<DrawingBrush.Drawing>
<DrawingGroup>
<DrawingGroup>
<GeometryDrawing Brush="{DynamicResource canvas}" Geometry="F1 M9,0 L9,9 L0,9 L0,0" />
</DrawingGroup>
<DrawingGroup>
<GeometryDrawing Brush="{DynamicResource outlining.chevron.foreground}" Geometry="{StaticResource ExpandDown}" />
</DrawingGroup>
</DrawingGroup>
</DrawingBrush.Drawing>
</DrawingBrush>
</Setter.Value>
</Setter>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
This is the style for the new chevron symbols. Here we find interesting things like the geometry for the expand/collapse symbols, the colors, and the trigger that changes the symbol when the control is expanded. The next step is to find the original Style
for the plus/minus symbols. For that we can either use an older VS installation or we can take advantage of the reference information above, which states that this control is included in the Microsoft.VisualStudio.Text.UI.Wpf v17.9.187
NuGet package. Now we know that the symbols changed in 17.9, so we will look for a version before that. For example, the last one before that is 17.8.222.
We can download the package, change its extension from .nupkg to .zip, and extract the assembly from the lib\net472
folder. Once we have the assembly, we can load it in ILSpy and look for the original style for the control just like we did before:
<Style x:Key="{x:Type textUiWpf:OutliningMarginHeaderControl}" TargetType="{x:Type textUiWpf:OutliningMarginHeaderControl}">
<Setter Property="Focusable" Value="False" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type textUiWpf:OutliningMarginHeaderControl}">
<Grid>
<Border Name="WhitePadding" Height="11" Width="9" BorderBrush="{DynamicResource ViewBackgroundBrush}" Background="{DynamicResource ViewBackgroundBrush}" BorderThickness="0,1,0,1" VerticalAlignment="Center">
<Border Name="TheSquare" Height="9" Width="9" BorderBrush="{DynamicResource outlining.verticalrule.foreground}" Background="{DynamicResource outlining.square.background}" BorderThickness="1">
<Canvas>
<Line X1="1" Y1="3.5" X2="6" Y2="3.5" Stroke="{DynamicResource outlining.square.foreground}" />
<Line Name="Vertical" X1="3.5" Y1="1" X2="3.5" Y2="6" Stroke="{DynamicResource outlining.square.foreground}" />
</Canvas>
</Border>
</Border>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="textUiWpf:OutliningMarginHeaderControl.IsExpanded" Value="True">
<Setter TargetName="Vertical" Property="Visibility" Value="Hidden" />
<Setter TargetName="TheSquare" Value="{DynamicResource ViewBackgroundBrush}" Property="Border.Background" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Since the new style no longer uses dynamic resources for outlining.square
, we must assume that these resources are no longer available in the new version of VS. This means that we needed to replace these resources with other colors. For the foreground outlining.chevron.foreground
made sense. The background was a little bit harder because outlining.chevron.background
didn’t do anything. With a little experimenting I had settled on outlining.collapsehintadornment.background
for the first version of the extension. Both were not ideal or what they used to be – the chevron foreground is quite a bit darker and the background for the plus symbol is a lot lighter than before, but this combination worked in both light and dark themes, and any other existing color resources would look wrong in one theme or another.
To override the style, I started by creating a new VSIX project with an async package:
Then I added a new ResourceDictionary file Style.xaml
to the project and copied the style from the old assembly, pasted it into the ResourceDictionary, and updated it with the new color resource value. Make sure that the Build Action
for this Style.xaml
is set to Page
. I also gave this style a key x:Key="OriginalOutliningMarginHeaderControlStyle"
, so that I can easily look it up later:
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:textUiWpf="clr-namespace:Microsoft.VisualStudio.Text.Editor;assembly=Microsoft.VisualStudio.Text.UI.Wpf">
<Style x:Key="OriginalOutliningMarginHeaderControlStyle" TargetType="textUiWpf:OutliningMarginHeaderControl">
...
The last thing that needed to be done was to override the style in the InitializeAsync
method of the package. The following code loads the resource dictionary, looks up the style by its key, and places it in the current application’s resources using the implicit style key typeof(OutliningMarginHeaderControl)
, so that Visual Studio will find this Style first:
protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress progress)
{
// When initialized asynchronously, the current thread may be a background thread at this point.
// Do any initialization that requires the UI thread after switching to the UI thread.
await this.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
var resourceDictionary = new ResourceDictionary();
resourceDictionary.Source = new Uri("pack://application:,,,/BringBackPlusMinus;component/Style.xaml");
var style = resourceDictionary["OriginalOutliningMarginHeaderControlStyle"];
Application.Current.Resources[typeof(OutliningMarginHeaderControl)] = style;
}
Building and running the project launched the experimental instance of Visual Studio and voila – the plus/minus symbols were back!
Now all that was left to do was to update the vsixmanifest, build the extension in Release mode, and upload it to the Visual Studio Marketplace following this Walkthrough: Publish a Visual Studio extension.
When I started using the preview builds of Visual Studio 2022 17.12, I noticed that the rendering of the plus/minus symbols was broken because the chevron symbol color definition had changed. I decided to introduce a dedicated color definition for the plus/minus symbols to avoid such problems going forward:
[Export(typeof(EditorFormatDefinition))]
[Name(OutliningExpanderIdentifier)]
[UserVisible(true)]
internal sealed class OutliningExpanderFormatDefinition : EditorFormatDefinition
{
public const string OutliningExpanderIdentifier = "outlining.plusminus";
public OutliningExpanderFormatDefinition()
{
this.ForegroundColor = Color.FromRgb(0x55, 0x55, 0x55);
this.BackgroundColor = Color.FromRgb(0xE2, 0xE2, 0xE2);
this.DisplayName = Strings.OutliningMarginPlusMinus;
}
}
As an added benefit the colors are now consistent with the original plus/minus symbols, and they can be customized in Tools -> Options -> Environment -> Fonts and Colors:
To make sure that the colors look correct in the dark theme as well, I added a ThemeColors.xml
file to the project with the following content:
<Themes>
<Theme Name="Dark" GUID="{1ded0138-47ce-435e-84ef-9ec1f439b749}">
<Category Name="BringBackPlusMinus" GUID="{063E9575-C1A8-4729-BB15-AAA2EFB44FC0}">
<Color Name="outlining.plusminus">
<Background Type="CT_RAW" Source="FF000000" />
<Foreground Type="CT_RAW" Source="FFE2E2E2" />
</Color>
</Category>
</Theme>
</Themes>
It needs to be converted to a .pkgdef file using the VsixColorCompiler first, which is then included in the extension’s .vsixmanifest.
In summary, with a little bit of detective work we were able to locate the style for the outlining symbols, build a VSIX extension, and override the style with the original style that brings back the original plus/minus outlining buttons.
The source code for the extension is available on GitHub.
The post The making of Bring Back Plus/Minus appeared first on Visual Studio Blog.
Say you want to do a case-insensitive substring search in a locale-aware manner. For example, maybe you have a list of names, and you want the user to be able to search for them by typing any name fragment. A search for “ber” could find “Bert” as well as “Roberta”.
I’ve seen people solve this problem by converting the string to lowercase, and then doing a code unit-based substring search. This technique doesn’t work for multiple reasons.
One reason is that some languages (like English) do not consider diacritics significant in collation. The word naive and naïve are considered equivalent for searching purposes. But a code unit substring search considers them different.
For languages in which diacritics are significant, you have the problem of composed and decomposed characters. For example, the lowercase a with ring in the Swedish word någon could be represented either as
The number of possibilities increases if you have characters with multiple diacritics. And then you also have ligatures, where the fi “fi” ligature is equivalent to two separate characters f and i.
So what’s the right thing to do?
In Windows, you can use the FindNLSStringEx
function to do a locale-aware substring search. Use the LINGUISTIC_IGNOREDIACRITIC
flag to say that you want to honor diacritics only when they are significant to the locale.¹ (A better name would have been LINGUISTIC_IGNOREINSIGNIFICANTDIACRITICS
.)
On other platforms, and even on Windows,² you can use the ICU library’s string search service and search with primary weight. (Primary weight honors diacritics which are significant to the locale.)
Bonus reading: A popular but wrong way to convert a string to uppercase or lowercase. What has case distinction but is neither uppercase nor lowercase?
¹ Throw in one of the IGNORECASE
flags if you want a case-sensitive substring search.
² The Windows globalization team now recommends that people use ICU, which has been part of Windows since Windows 10 version 1703 (build 15063). More details and gotchas here.
The post On locale-aware substring matching, either case-sensitive or case-insensitive appeared first on The Old New Thing.
Becky Carroll was missing a few teeth, others were stained or crooked. Ashamed, she smiled with lips pressed closed. Her dentist offered to fix most of her teeth with root canals and crowns, Carroll said, but she was wary of traveling a long road of dental work.
Then Carroll saw a TV commercial for another path: ClearChoice Dental Implant Centers. The company advertises that it can give patients “a new smile in as little as one day” by surgically replacing teeth instead of fixing them.
So Carroll saved and borrowed for the surgery, she said. In an interview and a lawsuit, Carroll said that at a ClearChoice clinic in New Jersey in 2021, she agreed to pay $31,000 to replace all her natural upper teeth with pearly white prosthetic ones. What came next, Carroll said, was “like a horror movie.”
Carroll alleged that her anesthesia wore off during implant surgery, so she became conscious as her teeth were removed and titanium screws were twisted into her jawbone. Afterward, Carroll’s prosthetic teeth were so misaligned that she was largely unable to chew for more than two years until she could afford corrective surgery at another clinic, according to a sworn deposition from her lawsuit.
ClearChoice has denied Carroll’s claims of malpractice and negligence in court filings and did not respond to requests for comment on the ongoing case.
“I thought implants would be easier, and all at once, so you didn’t have to keep going back to the dentist,” Carroll, 52, said in an interview. “But I should have asked more questions … like, Can they save these teeth?”
Dental implants have been used for more than half a century to surgically replace missing or damaged teeth with artificial duplicates, often with picture-perfect results. While implant dentistry was once the domain of a small group of highly trained dentists and specialists, tens of thousands of dental providers now offer the surgery and place millions of implants each year in the US.
Amid this booming industry, some implant experts worry that many dentists are losing sight of dentistry’s fundamental goal of preserving natural teeth and have become too willing to remove teeth to make room for expensive implants, according to a months-long investigation by KFF Health News and CBS News. In interviews, 10 experts said they had each given second opinions to multiple patients who had been recommended for mouths full of implants that the experts ultimately determined were not necessary. Separately, lawsuits filed across the country have alleged that implant patients like Carroll have experienced painful complications that have required corrective surgery, while other lawsuits alleged dentists at some implant clinics have persuaded, pressured, or forced patients to remove teeth unnecessarily.
The experts warn that implants, for a single tooth or an entire mouth, expose patients to costs and surgery complications, plus a new risk of future dental problems with fewer treatment options because their natural teeth are forever gone.
“There are many cases where teeth, they’re perfectly fine, and they’re being removed unnecessarily,” said William Giannobile, dean of the Harvard School of Dental Medicine. “I really hate to say it, but many of them are doing it because these procedures, from a monetary standpoint, they’re much more beneficial to the practitioner.”
Giannobile and nine other experts say they are combating a false public perception that implants are more durable and longer-lasting than natural teeth, which some believe stems in part from advertising on TV and social media. Implants require upkeep, and although they can’t get cavities, studies have shown that patients can be susceptible to infections in the gums and bone around their implants.
“Just because somebody can afford implants doesn’t necessarily mean that they’re a good candidate,” said George Mandelaris, a Chicago-area periodontist and member of the American Academy of Periodontology Board of Trustees. “When an implant has infection, or when an implant has bone loss, an implant dies a much quicker death than do teeth.”
In its simplest form, implant surgery involves extracting a single tooth and replacing it with a metal post that is screwed into the jaw and then affixed with a prosthetic tooth commonly made of porcelain, also known as a crown. Patients can also use “full-arch” or “All-on-4” implants to replace all their upper or lower teeth—or all their teeth.
For this story, KFF Health News and CBS News sought interviews with large dental chains whose clinics offer implant surgery—ClearChoice, Aspen Dental, Affordable Care, and Dental Care Alliance—each of which declined to be interviewed or did not respond to multiple requests for comment. The Association of Dental Support Organizations, which represents these companies and others like them, also declined an interview request.
ClearChoice, which specializes in full-arch implants, did not answer more than two dozen questions submitted in writing. In an emailed statement, the company said full-arch implants “have become a well-accepted standard of care for patients with severe tooth loss and teeth with poor prognosis.”
“The use of full-arch restorations reflects the evolution of modern dentistry, offering patients a solution that restores their ability to eat, speak, and live comfortably—far beyond what traditional dentures can provide,” the company said.
Carroll said she regrets not letting her dentist try to fix her teeth and rushing to ClearChoice for implants.
“Because it was a nightmare,” she said.
Dental implant surgery can be a godsend for patients with unsalvageable teeth. Several experts said implants can be so transformative that their invention should have contended for a Nobel Prize. And yet, these experts still worry that implants are overused, because it is generally better for patients to have their natural teeth.
Paul Rosen, a Pennsylvania periodontist who said he has worked with implants for more than three decades, said many patients believe a “fallacy” that implants are “bulletproof.”
“You can’t just have an implant placed and go off riding into the sunset,” Rosen said. “In many instances, they need more care than teeth because they are not teeth.”
Generally, a single implant costs a few thousand dollars while full-arch implants cost tens of thousands. Neither procedure is well covered by dental insurance, so many clinics partner with credit companies that offer loans for implant surgeries. At ClearChoice, for example, loans can be as large as $65,000 paid off over 10 years, according to the company’s website.
Despite the price, implants are more popular than ever. Sales increased by more than 6% on average each year since 2010, culminating in more than 3.7 million implants sold in the US in 2022, according to a 2023 report produced by iData Research, a health care market research firm.
Some worry implant dentistry has gone too far. In 10 interviews, dentists and dental specialists with expertise in implants said they had witnessed the overuse of implants firsthand. Each expert said they’d examined multiple patients in recent years who were recommended for full-arch implants by other dentists despite their teeth being treatable with conventional dentistry.
Giannobile, the Harvard dean, said he had given second opinions to “dozens” of patients who were recommended for implants they did not need.
“I see many of these patients now that are coming in and saying, ‘I’ve been seen, and they are telling me to get my entire dentition—all of my teeth—extracted.’ And then I’ll take a look at them and say that we can preserve most of your teeth,” Giannobile said.
Tim Kosinski, who is a representative of the Academy of General Dentistry and said he has placed more than 19,000 implants, said he examines as many as five patients a month who have been recommended for full-arch implants that he deems unnecessary.
“There is a push in the profession to remove teeth that could be saved,” Kosinski said. “But the public isn’t aware.”
Luiz Gonzaga, a periodontist and prosthodontist at the University of Florida, said he, too, had turned away patients who wanted most or all their teeth extracted. Gonzaga said some had received implant recommendations that he considered “an atrocity.”
“You don’t go to the hospital and tell them ‘I broke my finger a couple of times. This is bothering me. Can you please cut my finger off?’ No one will do that,” Gonzaga said. “Why would I extract your tooth because you need a root canal?”
Jaime Lozada, director of an elite dental implant residency program at Loma Linda University, said he’d not only witnessed an increase in dentists extracting “perfectly healthy teeth” but also treated a rash of patients with mouths full of ill-fitting implants that had to be surgically replaced.
Lozada said in August that he’d treated seven such patients in just three months.
“When individuals just make a decision of extracting teeth to make it simple and make money quick, so to speak, that’s where I have a problem,” Lozada said. “And it happens quite often.”
When full-arch implants fail, patients sometimes don’t have enough jawbone left to anchor another set. These patients have little choice but to get implants that reach into cheekbones, said Sohail Saghezchi, an oral and maxillofacial surgeon at the University of California-San Francisco.
“It’s kind of like a last resort,” Saghezchi said. “If those fail, you don’t have anywhere else to go.”
Most of the experts interviewed for this article said their rising alarm corresponded with big changes in the availability of dental implants. Implants are now offered by more than 70,000 dental providers nationwide, two-thirds of whom are general dentists, according to the iData Research report.
Dentists are not required to learn how to place implants in dental school, nor are they required to complete implant training before performing the surgery in nearly all states. This year, Oregon started requiring dentists to complete 56 hours of hands-on training before placing any implants. Stephen Prisby, executive director of the Oregon Board of Dentistry, said the requirement—the first and only of its kind in the US—was a response to dozens of investigations in the state into botched surgeries and other implant failures, split evenly between general dentists and specialists.
“I was frankly stunned at how bad some of these dentists were practicing,” Prisby said. “It was horrendous dentistry.”
Many dental clinics that offer implants have consolidated into chains owned by private equity firms that have bought out much of implant dentistry. In health care, private equity investment is sometimes criticized for overtreatment and prioritizing short-term profit over patients.
Private equity firms have spent about $5 billion in recent years to buy large dental chains that offer implants at hundreds of clinics owned by individual dentists and dental specialists. ClearChoice was bought for an estimated $1.1 billion in 2020 by Aspen Dental, which is owned by three private equity firms, according to PitchBook, a research firm focused on the private equity industry. Private equity firms also bought Affordable Care, whose largest clinic brand is Affordable Dentures & Implants, for an estimated $2.7 billion in 2021, according to PitchBook. And the private equity wing of the Abu Dhabi government bought Dental Care Alliance, which offers implants at many of its affiliated clinics, for an estimated $1 billion in 2022, according to PitchBook.
ClearChoice and Aspen Dental each said in email statements that the companies’ private equity owners “do not have influence or control over treatment recommendations.” Both companies said dentists or dental specialists make all clinical decisions.
Private equity deals involving dental practices increased ninefold from 2011 to 2021, according to an American Dental Association study published in August. The study also said investors showed an interest in oral surgery, possibly because of the “high prices” of implants.
“Some argue this is a negative thing,” said Marko Vujicic, vice president of the association’s Health Policy Institute, who co-authored the study. “On the other hand, some would argue that involvement of private equity and outside capital brings economies of scale, it brings efficiency.”
Edwin Zinman, a San Francisco dental malpractice attorney and former periodontist who has filed hundreds of dental lawsuits over four decades, said he believed many of the worst fears about private equity owners had already come true in implant dentistry.
“They’ve sold a lot of [implants], and some of it unnecessarily, and too often done negligently, without having the dentists who are doing it have the necessary training and experience,” Zinman said. “It’s for five simple letters: M-O-N-E-Y.”
For this article, journalists from KFF Health News and CBS News analyzed the webpages for more than 1,000 clinics in the nation’s largest private equity-owned dental chains, all of which offer some implants. The analysis found that more than 70% of those clinics listed only general dentists on their websites and did not appear to employ the specialists—oral surgeons, periodontists, or prosthodontists—who traditionally have more training with implants.
Affordable Dentures & Implants listed specialists at fewer than 5% of its more than 400 clinics, according to the analysis. The rest were staffed by general dentists, most of whom did not list credentialing from implant training organizations, according to the analysis.
ClearChoice, on the other hand, employs at least one oral surgeon or prosthodontist at each of its more than 100 centers, according to the analysis. But its new parent company, Aspen Dental, which offers implants in many of its more than 1,100 clinics, does not list any specialists at many of those locations.
Not everyone is worried about private equity in implant dentistry. In interviews arranged by the American Academy of Implant Dentistry, which trains dentists to use implants, two other implant experts did not express concerns about private equity firms.
Brian Jackson, a former academy president and implant specialist in New York, said he believed dentists are too ethical and patients are too smart to be pressured by private equity owners “who will never see a patient.”
Jumoke Adedoyin, a chief clinical officer for Affordable Care, who has placed implants at an Affordable Dentures & Implants clinic in the Atlanta suburbs for 15 years, said she had never felt pressure from above to sell implants.
“I’ve actually felt more pressure sometimes from patients who have gone around and been told they need to take their teeth out,” she said. “They come in and, honestly, taking a look at them, maybe they don’t need to take all their teeth out.”
Still, lawsuits filed across the country have alleged that dentists at implant clinics have extracted patients’ teeth unnecessarily.
For example, in Texas, a patient alleged in a 2020 lawsuit that an Affordable Care dentist removed “every single tooth from her mouth when such was not necessary,” then stuffed her mouth with gauze and left her waiting in the lobby as he and his staff left for lunch. In Maryland, a patient alleged in a 2021 lawsuit that ClearChoice “convinced” her to extract “eight healthy upper teeth,” by “greatly downplay[ing] the risks.” In Florida, a patient alleged in a 2023 lawsuit that ClearChoice provided her with no other treatment options before extracting all her teeth, “which was totally unnecessary.”
ClearChoice and Affordable Care denied wrongdoing in their respective lawsuits, then privately settled out of court with each patient. ClearChoice and Affordable Care did not respond to requests for comment submitted to the companies or attorneys. Lawyers for all three plaintiffs declined to comment on these lawsuits or did not respond to requests for comment.
Fred Goldberg, a Maryland dental malpractice attorney who said he has represented at least six clients who sued ClearChoice, said each of his clients agreed to get implants after meeting with a salesperson—not a dentist.
“Every client I’ve had who has gone to ClearChoice has started off meeting a salesperson and actually signing up to get their financing through ClearChoice before they ever meet with a dentist,” Goldberg said. “You meet with a salesperson who sells you on what they like to present as the best choice, which is almost always that they’re going to take out all your natural teeth.”
Becky Carroll, the ClearChoice patient from New Jersey, told a similar story.
Carroll said in her lawsuit that she met first with a ClearChoice salesperson referred to as a “patient education consultant.” In an interview, Carroll said the salesperson encouraged her to borrow money from family members for the surgery and it was not until after she agreed to a loan and passed a credit check that a ClearChoice dentist peered into her mouth.
“It seems way backwards,” Carroll said. “They just want to know you’re approved before you get to talk to a dentist.”
CBS News producer Nicole Keller contributed to this report.
This story originally appeared on KFF Health News, a national newsroom that produces in-depth journalism about health issues and is one of the core operating programs at KFF—an independent source of health policy research, polling, and journalism. Learn more about KFF.
Read more of this story at Slashdot.